/* ===================================================
 * tagmanager.js v3.0.0
 * http://welldonethings.com/tags/manager
 * ===================================================
 * Copyright 2012 Max Favilli
 *
 * Licensed under the Mozilla Public License, Version 2.0 You may not use this work except in compliance with the License.
 *
 * http://www.mozilla.org/MPL/2.0/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ========================================================== */
(function ($)
{

	"use strict";

	var defaults = {
		prefilled: null,
		CapitalizeFirstLetter: false,
		preventSubmitOnEnter: true,     // deprecated
		isClearInputOnEsc: true,        // deprecated
		AjaxPush: null,
		AjaxPushAllTags: null,
		AjaxPushParameters: null,
		delimiters: [9, 13, 44],        // tab, enter, comma
		backspace: [8],
		maxTags: 0,
		hiddenTagListName: null,        // deprecated
		hiddenTagListId: null,          // deprecated
		replace: true,
		output: null,
		deleteTagsOnBackspace: true,    // deprecated
		tagsContainer: null,
		tagCloseIcon: 'x',
		tagClass: '',
		validator: null,
		onlyTagList: false
	},

    publicMethods = {
    	pushTag: function (tag, ignoreEvents)
    	{
    		var $self = $(this), opts = $self.data('opts'), alreadyInList, tlisLowerCase, max, tagId,
            tlis = $self.data("tlis"), tlid = $self.data("tlid"), idx, newTagId, newTagRemoveId, escaped,
            html, $el, lastTagId, lastTagObj;

    		tag = privateMethods.trimTag(tag, opts.delimiterChars);

    		if (!tag || tag.length <= 0) { return; }

    		if (opts.CapitalizeFirstLetter && tag.length > 1)
    		{
    			tag = tag.charAt(0).toUpperCase() + tag.slice(1).toLowerCase();
    		}

    		// call the validator (if any) and do not let the tag pass if invalid
    		if (opts.validator && !opts.validator(tag)) { return; }

    		// dont accept new tags beyond the defined maximum
    		if (opts.maxTags > 0 && tlis.length >= opts.maxTags) { return; }

    		alreadyInList = false;
    		//use jQuery.map to make this work in IE8 (pure JS map is JS 1.6 but IE8 only supports JS 1.5)
    		tlisLowerCase = jQuery.map(tlis, function (elem)
    		{
    			return elem.toLowerCase();
    		});

    		idx = $.inArray(tag.toLowerCase(), tlisLowerCase);

    		if (-1 !== idx)
    		{
    			// console.log("tag:" + tag + " !!already in list!!");
    			alreadyInList = true;
    		}

    		if (alreadyInList)
    		{
    			$self.trigger('tm:duplicated', tag);
    			$("#" + $self.data("tm_rndid") + "_" + tlid[idx]).stop()
                    .animate({ backgroundColor: opts.blinkBGColor_1 }, 100)
                    .animate({ backgroundColor: opts.blinkBGColor_2 }, 100)
                    .animate({ backgroundColor: opts.blinkBGColor_1 }, 100)
                    .animate({ backgroundColor: opts.blinkBGColor_2 }, 100)
                    .animate({ backgroundColor: opts.blinkBGColor_1 }, 100)
                    .animate({ backgroundColor: opts.blinkBGColor_2 }, 100);
    		} else
    		{
    			if (!ignoreEvents) { $self.trigger('tm:pushing', tag); }

    			max = Math.max.apply(null, tlid);
    			max = max === -Infinity ? 0 : max;

    			tagId = ++max;
    			tlis.push(tag);
    			tlid.push(tagId);

    			if (!ignoreEvents)
    				if (opts.AjaxPush !== null)
    				{
    					if ($.inArray(tag, opts.prefilled) === -1)
    					{
    						$.post(opts.AjaxPush, $.extend({ tag: tag }, opts.AjaxPushParameters));
    					}
    				}

    			// console.log("tagList: " + tlis);

    			newTagId = $self.data("tm_rndid") + '_' + tagId;
    			newTagRemoveId = $self.data("tm_rndid") + '_Remover_' + tagId;
    			escaped = $("<span/>").text(tag).html();

    			html = '<span class="' + privateMethods.tagClasses.call($self) + '" id="' + newTagId + '">';
    			html += '<span>' + escaped + '</span>';
    			html += '<a href="#" class="tm-tag-remove" id="' + newTagRemoveId + '" TagIdToRemove="' + tagId + '">';
    			html += opts.tagCloseIcon + '</a></span> ';
    			$el = $(html);

    			if (opts.tagsContainer !== null)
    			{
    				$(opts.tagsContainer).append($el);
    			} else
    			{
    				if (tagId > 1)
    				{
    					lastTagId = tagId - 1;
    					lastTagObj = $("#" + $self.data("tm_rndid") + "_" + lastTagId);
    					lastTagObj.after($el);
    				} else
    				{
    					$self.before($el);
    				}
    			}

    			$el.find("#" + newTagRemoveId).on("click", $self, function (e)
    			{
    				e.preventDefault();
    				var TagIdToRemove = parseInt($(this).attr("TagIdToRemove"));
    				privateMethods.spliceTag.call($self, TagIdToRemove, e.data);
    			});

    			privateMethods.refreshHiddenTagList.call($self);

    			if (!ignoreEvents) { $self.trigger('tm:pushed', tag); }

    			privateMethods.showOrHide.call($self);
    			//if (tagManagerOptions.maxTags > 0 && tlis.length >= tagManagerOptions.maxTags) {
    			//  obj.hide();
    			//}
    		}
    		$self.val("");
    	},

    	popTag: function ()
    	{
    		var $self = $(this), tagId, tagBeingRemoved,
            tlis = $self.data("tlis"),
            tlid = $self.data("tlid");

    		if (tlid.length > 0)
    		{
    			tagId = tlid.pop();

    			tagBeingRemoved = tlis[tlis.length - 1];
    			$self.trigger('tm:popping', tagBeingRemoved);
    			tlis.pop();

    			// console.log("TagIdToRemove: " + tagId);
    			$("#" + $self.data("tm_rndid") + "_" + tagId).remove();
    			privateMethods.refreshHiddenTagList.call($self);
    			$self.trigger('tm:popped', tagBeingRemoved);
    			// console.log(tlis);
    		}
    	},

    	empty: function ()
    	{
    		var $self = $(this), tlis = $self.data("tlis"), tlid = $self.data("tlid"), tagId;

    		while (tlid.length > 0)
    		{
    			tagId = tlid.pop();
    			tlis.pop();
    			// console.log("TagIdToRemove: " + tagId);
    			$("#" + $self.data("tm_rndid") + "_" + tagId).remove();
    			privateMethods.refreshHiddenTagList.call($self);
    			// console.log(tlis);
    		}
    		$self.trigger('tm:emptied', null);

    		privateMethods.showOrHide.call($self);
    		//if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
    		//  obj.show();
    		//}
    	}
    },

    privateMethods = {
    	showOrHide: function ()
    	{
    		var $self = this, opts = $self.data('opts'), tlis = $self.data("tlis");

    		if (opts.maxTags > 0 && tlis.length < opts.maxTags)
    		{
    			$self.show();
    			$self.trigger('tm:show');
    		}

    		if (opts.maxTags > 0 && tlis.length >= opts.maxTags)
    		{
    			$self.hide();
    			$self.trigger('tm:hide');
    		}
    	},

    	tagClasses: function ()
    	{
    		var $self = $(this), opts = $self.data('opts'), tagBaseClass = opts.tagBaseClass,
            inputBaseClass = opts.inputBaseClass, cl;
    		// 1) default class (tm-tag)
    		cl = tagBaseClass;
    		// 2) interpolate from input class: tm-input-xxx --> tm-tag-xxx
    		if ($self.attr('class'))
    		{
    			$.each($self.attr('class').split(' '), function (index, value)
    			{
    				if (value.indexOf(inputBaseClass + '-') !== -1)
    				{
    					cl += ' ' + tagBaseClass + value.substring(inputBaseClass.length);
    				}
    			});
    		}
    		// 3) tags from tagClass option
    		cl += (opts.tagClass ? ' ' + opts.tagClass : '');
    		return cl;
    	},

    	trimTag: function (tag, delimiterChars)
    	{
    		var i;
    		tag = $.trim(tag);
    		// truncate at the first delimiter char
    		i = 0;
    		for (i; i < tag.length; i++)
    		{
    			if ($.inArray(tag.charCodeAt(i), delimiterChars) !== -1) { break; }
    		}
    		return tag.substring(0, i);
    	},

    	refreshHiddenTagList: function ()
    	{
    		var $self = $(this), tlis = $self.data("tlis"), lhiddenTagList = $self.data("lhiddenTagList");

    		if (lhiddenTagList)
    		{
    			$(lhiddenTagList).val(tlis.join($self.data('opts').baseDelimiter)).change();
    		}

    		$self.trigger('tm:refresh', tlis.join($self.data('opts').baseDelimiter));
    	},

    	killEvent: function (e)
    	{
    		e.cancelBubble = true;
    		e.returnValue = false;
    		e.stopPropagation();
    		e.preventDefault();
    	},

    	keyInArray: function (e, ary)
    	{
    		return $.inArray(e.which, ary) !== -1;
    	},

    	applyDelimiter: function (e)
    	{
    		var $self = $(this);
    		publicMethods.pushTag.call($self, $(this).val());
    		e.preventDefault();
    	},

    	prefill: function (pta)
    	{
    		var $self = $(this);
    		$.each(pta, function (key, val)
    		{
    			publicMethods.pushTag.call($self, val, true);
    		});
    	},

    	pushAllTags: function (e, tag)
    	{
    		var $self = this, opts = $self.data('opts'), tlis = $self.data("tlis");
    		if (opts.AjaxPushAllTags)
    		{
    			if (e.type !== 'tm:pushed' || $.inArray(tag, opts.prefilled) === -1)
    			{
    				$.post(opts.AjaxPush, { tags: tlis.join(opts.baseDelimiter) });
    			}
    		}
    	},

    	spliceTag: function (tagId)
    	{
    		var $self = this, tlis = $self.data("tlis"), tlid = $self.data("tlid"), idx = $.inArray(tagId, tlid),
            tagBeingRemoved;

    		// console.log("TagIdToRemove: " + tagId);
    		// console.log("position: " + idx);

    		if (-1 !== idx)
    		{
    			tagBeingRemoved = tlis[idx];
    			$self.trigger('tm:splicing', tagBeingRemoved);
    			$("#" + $self.data("tm_rndid") + "_" + tagId).remove();
    			tlis.splice(idx, 1);
    			tlid.splice(idx, 1);
    			privateMethods.refreshHiddenTagList.call($self);
    			$self.trigger('tm:spliced', tagBeingRemoved);
    			// console.log(tlis);
    		}

    		privateMethods.showOrHide.call($self);
    		//if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
    		//  obj.show();
    		//}
    	},

    	init: function (options)
    	{
    		var opts = $.extend({}, defaults, options), delimiters, keyNums;

    		opts.hiddenTagListName = (opts.hiddenTagListName === null)
                ? 'hidden-' + this.attr('name')
                : opts.hiddenTagListName;

    		delimiters = opts.delimeters || opts.delimiters; // 'delimeter' is deprecated
    		keyNums = [9, 13, 17, 18, 19, 37, 38, 39, 40]; // delimiter values to be handled as key codes
    		opts.delimiterChars = [];
    		opts.delimiterKeys = [];

    		$.each(delimiters, function (i, v)
    		{
    			if ($.inArray(v, keyNums) !== -1)
    			{
    				opts.delimiterKeys.push(v);
    			} else
    			{
    				opts.delimiterChars.push(v);
    			}
    		});

    		opts.baseDelimiter = String.fromCharCode(opts.delimiterChars[0] || 44);
    		opts.tagBaseClass = 'tm-tag';
    		opts.inputBaseClass = 'tm-input';

    		if (!$.isFunction(opts.validator)) { opts.validator = null; };

    		this.each(function ()
    		{
    			var $self = $(this), hiddenObj = '', rndid = '', albet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    			// prevent double-initialization of TagManager
    			if ($self.data('tagManager')) { return false; }
    			$self.data('tagManager', true);

    			for (var i = 0; i < 5; i++)
    			{
    				rndid += albet.charAt(Math.floor(Math.random() * albet.length));
    			}

    			$self.data("tm_rndid", rndid);

    			// store instance-specific data in the DOM object
    			$self.data('opts', opts)
                    .data('tlis', []) //list of string tags
                    .data('tlid', []); //list of ID of the string tags

    			if (opts.output === null)
    			{
    				hiddenObj = $('<input/>', {
    					type: 'hidden',
    					name: opts.hiddenTagListName
    				});

    				$self.after(hiddenObj);
    				$self.data("lhiddenTagList", hiddenObj);
    			} else
    			{
    				$self.data("lhiddenTagList", $(opts.output));
    			}

    			if (opts.AjaxPushAllTags)
    			{
    				$self.on('tm:spliced', privateMethods.pushAllTags);
    				$self.on('tm:popped', privateMethods.pushAllTags);
    				$self.on('tm:pushed', privateMethods.pushAllTags);
    			}

    			// hide popovers on focus and keypress events
    			$self.on('focus keypress', function (e)
    			{
    				if ($(this).popover) { $(this).popover('hide'); }
    			});

    			// handle ESC (keyup used for browser compatibility)
    			if (opts.isClearInputOnEsc)
    			{
    				$self.on('keyup', function (e)
    				{
    					if (e.which === 27)
    					{
    						// console.log('esc detected');
    						$(this).val('');
    						privateMethods.killEvent(e);
    					}
    				});
    			}

    			$self.on('keypress', function (e)
    			{
    				// push ASCII-based delimiters
    				if (privateMethods.keyInArray(e, opts.delimiterChars))
    				{
    					privateMethods.applyDelimiter.call($self, e);
    				}
    			});

    			$self.on('keydown', function (e)
    			{
    				// disable ENTER
    				if (e.which === 13)
    				{
    					if (opts.preventSubmitOnEnter)
    					{
    						privateMethods.killEvent(e);
    					}
    				}

    				// push key-based delimiters (includes <enter> by default)
    				if (privateMethods.keyInArray(e, opts.delimiterKeys))
    				{
    					privateMethods.applyDelimiter.call($self, e);
    				}
    			});

    			// BACKSPACE (keydown used for browser compatibility)
    			if (opts.deleteTagsOnBackspace)
    			{
    				$self.on('keydown', function (e)
    				{
    					if (privateMethods.keyInArray(e, opts.backspace))
    					{
    						// console.log("backspace detected");
    						if ($(this).val().length <= 0)
    						{
    							publicMethods.popTag.call($self);
    							privateMethods.killEvent(e);
    						}
    					}
    				});
    			}

    			$self.change(function (e)
    			{
    				if (!/webkit/.test(navigator.userAgent.toLowerCase()))
    				{
    					$self.focus();
    				} // why?

    				/* unimplemented mode to push tag on blur
                     else if (tagManagerOptions.pushTagOnBlur) {
                     console.log('change: pushTagOnBlur ' + tag);
                     pushTag($(this).val());
                     } */
    				privateMethods.killEvent(e);
    			});

    			if (opts.prefilled !== null)
    			{
    				if (typeof (opts.prefilled) === "object")
    				{
    					privateMethods.prefill.call($self, opts.prefilled);
    				} else if (typeof (opts.prefilled) === "string")
    				{
    					privateMethods.prefill.call($self, opts.prefilled.split(opts.baseDelimiter));
    				} else if (typeof (opts.prefilled) === "function")
    				{
    					privateMethods.prefill.call($self, opts.prefilled());
    				}
    			} else if (opts.output !== null)
    			{
    				if ($(opts.output) && $(opts.output).val()) { var existing_tags = $(opts.output); }
    				privateMethods.prefill.call($self, $(opts.output).val().split(opts.baseDelimiter));
    			}

    		});

    		return this;
    	}
    };

	$.fn.tagsManager = function (method)
	{
		var $self = $(this);

		if (!(0 in this)) { return this; }

		if (publicMethods[method])
		{
			return publicMethods[method].apply($self, Array.prototype.slice.call(arguments, 1));
		} else if (typeof method === 'object' || !method)
		{
			return privateMethods.init.apply(this, arguments);
		} else
		{
			$.error('Method ' + method + ' does not exist.');
			return false;
		}
	};

}(jQuery));
